using System;
using System.Collections;
using System.Diagnostics;

namespace Pharmacy.Ultilities
{
    /// <summary>
    /// Provides multi-keys caching.
    /// </summary>
    /// <author>ThangTran</author>
    /// <history>
    /// 2011.03.04 - ThangTran: added.
    /// 2011.04.05 - ThangTran: added function GetCachedValue().
    /// 2011.04.14 - ThangTran: added functions SetCachedValue(), GetCachedValueOrDefault().
    /// </history>
    public static class CacheManager
    {
        /// <summary>
        /// Gets or sets table of cached data.
        /// </summary>
        private static readonly Hashtable Cache = new Hashtable();

        /// <summary>
        /// Gets cached value of given set of keys. 
        /// If one the keys is not found, the keys will be created.
        /// If the value is not added, it will be re-initialized by given initializer(), 
        /// if the initializer() is null, not value is added.
        /// </summary>
        /// <typeparam name="T">Type of the result.</typeparam>
        /// <param name="initializer">Reference to the initializer. This is an optional parameter.</param>
        /// <param name="keys">Set of keys to access the value.</param>
        /// <returns></returns>
        public static T GetValue<T>(Func<T> initializer, params object[] keys)
        {
            // get lastest key of given set of keys
            object lastKey = keys[keys.Length - 1];

            // get the hash table that contains the lastest key
            Hashtable table = GetContainingHashtable(true, keys);

            // try to get the weak reference of the value
            if (table.ContainsKey(lastKey))
            {
                return (T) table[lastKey];
            }

            // create new instance of value
            if (initializer != null)
            {
                // call initializer() to initialize new value
                T value = initializer();

                // add to hashtable
                table.Add(lastKey, value);

                // return created value
                return value;
            }

            return default(T);
        }

        /// <summary>
        /// Gets cached value of given set of keys. 
        /// If one of the keys is not found OR the value is not added, an exception will be thrown, 
        /// </summary>
        /// <typeparam name="T">Type of the result.</typeparam>
        /// <param name="keys">Set of keys to access the value.</param>
        /// <returns></returns>
        public static T GetCachedValue<T>(params object[] keys)
        {
            // get lastest key of given set of keys
            object lastKey = keys[keys.Length - 1];

            // get the hash table that contains the lastest key
            Hashtable table = GetContainingHashtable(false, keys);

            // try to get the weak reference of the value
            if (table != null && table.ContainsKey(lastKey))
            {
                return (T)table[lastKey];
            }

            // value not found
            throw ExceptionHelper
                .From(new Exception("Cached value not found."))
                .DataRange("keys", keys)
                .ToException();
        }

        /// <summary>
        /// Gets cached value of given set of keys. 
        /// If one of the keys is not found OR the value is not added, the default value will be returned, 
        /// </summary>
        /// <typeparam name="T">Type of the result.</typeparam>
        /// <param name="keys">Set of keys to access the value.</param>
        /// <returns></returns>
        public static T GetCachedValueOrDefault<T>(params object[] keys)
        {
            if (ContainsKey(keys))
            {
                return GetCachedValue<T>(keys);
            }
            return default(T);
        }

        /// <summary>
        /// Sets cached value of given set of keys. 
        /// If one the keys is not found, the keys will be created.
        /// </summary>
        /// <param name="value"></param>
        /// <param name="keys">Set of keys to access the value.</param>
        /// <returns></returns>
        public static void SetCachedValue(object value, params object[] keys)
        {
            // get lastest key of given set of keys
            object lastKey = keys[keys.Length - 1];

            // get the hash table that contains the lastest key
            Hashtable table = GetContainingHashtable(true, keys);

            // try to get the weak reference of the value
            if (table.ContainsKey(lastKey))
            {
                table[lastKey] = value;
                return;
            }

            //else

            // add to hashtable
            table.Add(lastKey, value);
        }

        /// <summary>
        /// Clears all cached data.
        /// </summary>
        public static void Clear()
        {
            Cache.Clear();
        }

        /// <summary>
        /// Indicates the mananager contains given set of keys or not. 
        /// </summary>
        /// <param name="keys">Set of keys to access the value.</param>
        /// <returns></returns>
        public static bool ContainsKey(params object[] keys)
        {
            // get lastest key of given set of keys
            object lastKey = keys[keys.Length - 1];

            // get the hash table that contains the lastest key
            Hashtable table = GetContainingHashtable(false, keys);

            // try to get the weak reference of the value
            return table != null && table.ContainsKey(lastKey);
        }

        /// <summary>
        /// Removes cached value of given set of keys. 
        /// If one of the keys is not found OR the value is not added, an exception will be thrown, 
        /// </summary>
        /// <param name="keys">Set of keys to access the value.</param>
        /// <returns></returns>
        public static void Remove(params object[] keys)
        {
            // get lastest key of given set of keys
            object lastKey = keys[keys.Length - 1];

            // get the hash table that contains the lastest key
            Hashtable table = GetContainingHashtable(false, keys);

            // try to get the weak reference of the value
            if (table != null && table.ContainsKey(lastKey))
            {
                table.Remove(lastKey);
                return;
            }

            // value not found
            throw ExceptionHelper
                .From(new Exception("Cached value not found."))
                .DataRange("keys", keys)
                .ToException();
        }        

        /// <summary>
        /// Gets the hashtable contains given lastest keys.
        /// </summary>
        /// <param name="createNonExistedKeys">Indicates that the keys will be created if they are not existed.</param>
        /// <param name="keys">Set of keys to access the value.</param>
        /// <returns></returns>
        private static Hashtable GetContainingHashtable(bool createNonExistedKeys, params object[] keys)
        {
            // --- Begin parameters checking code -----------------------------
            Debug.Assert(keys != null);
            Debug.Assert(keys.Length >= 1);
            // --- End parameters checking code -------------------------------

            // prepare current hashtable
            Hashtable ret = Cache;

            // for to the 2nd lastest key
            for (int i = 0; i < keys.Length - 1; i++)
            {
                // get key of current path
                object key = keys[i];

                // check if the table already has the current key
                if (ret.ContainsKey(key))
                {
                    // move deeper
                    ret = (Hashtable)ret[key];
                }
                else
                {
                    // the key does not exist
                    if (createNonExistedKeys)
                    {
                        // create new table and move deeper
                        ret.Add(key, new Hashtable());
                        ret = (Hashtable)ret[key];
                    }
                    else
                    {
                        // task terminated
                        return null;
                    }
                }
            }

            return ret;
        }
    }
}
